package dk.au.cs.peterbn.patterns.impl;

import dk.au.cs.peterbn.patterns.CallSite;
import dk.au.cs.peterbn.patterns.TypeRole;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.output.XMLOutputter;

import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A factory for PatternImplementations
 */
public class DesignPatternImplFactory {

    public static final Namespace PATTERNS = Namespace.getNamespace("pattern", "http://cs.au.dk/peterbn/patterns");
    public static final Namespace VARIATION = Namespace.getNamespace("var", "http://cs.au.dk/peterbn/patterns/variation");
    public static final Namespace XHTML = Namespace.getNamespace("xhtml", "http://www.w3.org/1999/xhtml");

    private DesignPatternImpl pattern;


    /**
     * Builds a design pattern from a Document
     *
     *
     * @param doc The source document
     */
    public void buildDesignPattern(Document doc) {
        pattern = new DesignPatternImpl();
        Element rootElement = doc.getRootElement();
        String patternName = rootElement.getAttributeValue("name");
        String group = rootElement.getAttributeValue("group");
        pattern.setName(patternName);
        pattern.setGroup(group);
        if (rootElement.getChild("description", PATTERNS) != null) {
            setDescription(rootElement, pattern);
        } else {
            throw new InvalidPatternDescriptionException("A base pattern description must have a description element.");
        }


        addOrUpdateRoles(rootElement);

    }

    /**
     * Apply a pattern variation to the product pattern.
     * @param doc The pattern variation descriptor
     */
    public void applyVariation(Document doc) {
        Element rootElement = doc.getRootElement();
        String patternName = rootElement.getAttributeValue("name");
        String group = rootElement.getAttributeValue("group");
        Element varElement = rootElement.getChild("variation", VARIATION);
        if (varElement == null) {
            throw new InvalidPatternDescriptionException("Missing variation element in pattern variation descriptor");
        }
        String variationName = varElement.getAttributeValue("name");
        if (!(patternName.equals(pattern.getName()) && group.equals(pattern.getGroup()))) {
            throw new InvalidPatternDescriptionException("The pattern variation " + variationName + " for " + group + ":" + patternName + " Does not apply to " +pattern.getIdentifier());
        }

        addVariation(varElement);
        addOrUpdateRoles(rootElement);


    }

    private void addOrUpdateRoles(Element rootElement) {
        Element rolesElement = rootElement.getChild("roles", PATTERNS);
        //noinspection unchecked
        List<Element> roles = rolesElement.getChildren("role", PATTERNS);
        Map<String, TypeRoleImpl> oldRoles = pattern.getAndClerRoles();
        for (Element role : roles) {
            addOrUpdateTypeRole(oldRoles, role);
        }
    }

    private void addOrUpdateTypeRole(Map<String, TypeRoleImpl> oldRoles, Element role) {
        String name = role.getAttributeValue("name");
        String targetName = role.getAttributeValue("target");
        TypeRole.Target target = TypeRole.Target.get(targetName);

        TypeRoleImpl typeRole = oldRoles.get(name);
        if (typeRole == null) {
            typeRole = new TypeRoleImpl(pattern, name, target);
        } else if (target != null) {
            //The target may be updated
            typeRole.setTarget(target);
        }
        pattern.addTypeRole(typeRole);


        setDescription(role, typeRole);

        Element membersElement = role.getChild("members", PATTERNS);
        if (membersElement != null) {
            Map<String,MemberRoleImpl> oldMembers = typeRole.getAndClearOwnMembers();
            //noinspection unchecked
            List<Element> members = membersElement.getChildren();
            for (Element member : members) {
                addOrUpdateMemberRole(typeRole,oldMembers, member);
            }
        }

        Element relationsElement = role.getChild("relations", PATTERNS);
        if (relationsElement != null) {
            //noinspection unchecked
            List<Element> relations = relationsElement.getChildren();
            typeRole.clearRelationProtos();
            for (Element relation : relations) {
                String type = relation.getName();
                String relTarget = relation.getAttributeValue("ref");
                typeRole.addRelationProto(new RelationProto(type, relTarget));
            }
        }
    }

    private void addOrUpdateMemberRole(TypeRoleImpl typeRole, Map<String, MemberRoleImpl> oldMembers, Element member) {
        String name = member.getAttributeValue("name");
        MemberRoleImpl ownMember = oldMembers.get(name);
        if (ownMember == null) {
            ownMember = new MemberRoleImpl(this.pattern, name);
        }
        typeRole.addMember(ownMember);
        setDescription(member, ownMember);

        Element callinfo = member.getChild("callinfo",PATTERNS);
        if (callinfo != null) {
            Set<CallSite> oldCallSites = ownMember.getCallSites();
            ownMember.clearCallSites();
            addOrUpdateCallSites(ownMember, callinfo);

        }
    }

    private void addOrUpdateCallSites(MemberRoleImpl member, Element callinfo) {
        List<Element> callSites = callinfo.getChildren("callsite", PATTERNS);
        for (Element callSite : callSites) {
            CallSiteImpl site = new CallSiteImpl(pattern);
            member.addCallSite(site);
            String restriction = callSite.getAttributeValue("restriction");
            site.setRestriction(CallSite.Restriction.valueOf(restriction.toUpperCase()));
            String typeVal = callSite.getAttributeValue("type");
            site.setType(typeVal);
            String memberVal = callSite.getAttributeValue("member");
            site.setMember(memberVal);
        }
    }

    /**
     * Reads and sets the description from the dom.
     *
     * @param element     The DOM element.
     * @param describable The element to set the description on.
     */
    private void setDescription(Element element, HasModifiableDescription describable) {
        //parse description
        Element description = element.getChild("description", PATTERNS);
        if (description != null) {
            String descriptionString = getContentStringFormatted(description);
            describable.setDescription(descriptionString);
        }
    }

    /**
     * Serializes the content of the element to a string.
     * @param element The element whose content should be serialized
     * @return The string representation.
     */
    private String getContentStringFormatted(Element element) {
        List descriptionContent = element.getContent();
        XMLOutputter xmlOutputter = new XMLOutputter();
        StringWriter stringWriter = new StringWriter();
        try {
            xmlOutputter.output(descriptionContent, stringWriter);
        } catch (IOException e) {
            //yeah, not happening
            throw new RuntimeException(e);
        }
        return stringWriter.toString();
    }

    public DesignPatternImpl getPattern() {
        return pattern;
    }

    private void addVariation(Element varElement) {
        String variationName = varElement.getAttributeValue("name");
        Element rationale = varElement.getChild("rationale", VARIATION);
        String rationaleString = getContentStringFormatted(rationale);
        PatternVariationImpl patternVariation = new PatternVariationImpl(variationName, rationaleString);
        pattern.addVariation(patternVariation);
    }
}
